Python decimalモジュールの丸め処理を使い分ける
Pythonには10進の浮動小数点の計算を正確におこなうためのdecimalモジュールが存在します(JavaのBigDecimalクラスに相当)。
Python2.7.11/3.5.1 時点では以下の丸め処理に対応しています。
- ROUND_CEILING
- ROUND_FLOOR
- ROUND_UP
- ROUND_05UP
- ROUND_DOWN
- ROUND_HALF_UP
- ROUND_HALF_DOWN
- ROUND_HALF_EVEN
名前からなんとなく処理方法を選択し、後になって計算が合わなくて頭を抱えなくて済むように、それぞれの動きの違いを確認してみましょう。
丸め処理の違い
CEILING/FLOOR 系の違い
この2つは対極です。
- ROUND_CEILINGは+∞にむけて丸めます
- ROUND_FLOORは-∞にむけて丸めます
0.51 を精度1桁で丸める場合、
- ROUND_CEILINGは切り上げて 0.6
- ROUND_FLOOR は切り捨てて 0.5
となります。
-0.51 を精度1桁で丸める場合、
- ROUND_CEILINGは切り上げて -0.5
- ROUND_FLOOR は切り捨てて -0.6
となります。
UP/DOWN 系
この2つは対極です。
- ROUND_UPは0から離れるように丸めます
- ROUND_DOWNは0に近づくように丸めます
0.51 を精度1桁で丸める場合、
- ROUND_UPは 0.6、つまり、ROUND_CIELINGと同じ
- ROUND_DOWNは 0.5、つまり、ROUND_FLOORと同じ
となります。
-0.51 を精度1桁で丸める場合、
- ROUND_UPは -0.6、つまり、ROUND_FLOORと同じ
- ROUND_DOWNは -0.5、つまり、ROUND_CEILINGと同じ
となります。正負によって、UP/DOWNとCEILING/FLOORの対応関係が変わります。注意してください。
HALF 系
最も近い数字に丸めます。
ROUND_HALF_UP の場合は、両隣の数字の近い方に丸め、両隣の数字が等距離の場合は UP の振る舞いをします。
0.55 を精度1桁で丸める場合、両隣の数字が5と6で等距離にあるため、UP 丸めされて、6となります。
ROUND_HALF_DOWN も同様です。
ROUND_HALF_EVEN の場合は、両隣の数字の近い方に丸め、両隣の数字が等距離の場合は偶数(even)の数字に丸めます。
0.55 を精度1桁で丸める場合、両隣の数字が 5と6で等距離にあるため、偶数の6に丸められます。0.45の場合は、4と5で等距離にあるため、偶数の4に丸められます。
"ROUND_HALF_EVEN" は "banker's rounding(銀行丸め)"とも呼ばれ、.NET のMath.Round の標準の丸め方式です。 Math.Round のリファレンスからは、一方向に丸めたときの誤差の累積を嫌ってこのようになっていると読み取れます。
When used in multiple rounding operations, it reduces the rounding error that is caused by consistently rounding midpoint values in a single direction. In some cases, this rounding error can be significant. https://msdn.microsoft.com/en-us/library/system.math.round.aspx#Midpoint
ROUND_05UP
最後に残ったのが ROUND_05UP です。
0に近づくように丸めたとして(ROUND_DOWN)、最後の数字が 0 または 5 になる場合、0から離れる丸め方式(ROUND_UP)に変更して丸めます。 丸めた後の最後の数字が 0でも5でもなければ、「0から離れる丸め処理」は発生しません。ROUND_DOWNのままとします。 丸める対象の数字が桁の場合は桁を落とすだけです。
0.51 を精度1桁に丸める場合、2つ目の1を0に近づくように丸めるとしたら「0.5」となります。 最後の数字は「5」のため、「0.51」をROUND_UP方式で丸め「0.6」となります。
0.61 を精度1桁に丸める場合、2つ目の1を0に近づくように丸めて「0.6」となります。 最後の数字は「6」であり、0でも5でもないため、ROUND_DOWN方式のまま「0.6」となります。
0.61をROUND_UP で丸めると、2つ目の1を0から離れるように丸めて「0.7」となります。「0.61」はROUND_UPとROUND_05UPで結果が異なります。
丸めモードの違いと丸めた結果を表で整理
以上の説明を元に、いくつかの入力値に対して、丸めモードごとの丸め処理の違いを整理しました。次の表では精度は全て1桁です。
似通った丸めモードも、結果がところどころ違っていることに気づいていただけるかと思います。
Mode. | ROUND_CEILING | ROUND_FLOOR | ROUND_UP | ROUND_05UP | ROUND_DOWN | ROUND_HALF_UP | ROUND_HALF_DOWN | ROUND_HALF_EVEN |
---|---|---|---|---|---|---|---|---|
+0.61 | 0.7 | 0.6 | 0.7 | 0.6 | 0.6 | 0.6 | 0.6 | 0.6 |
+0.55 | 0.6 | 0.5 | 0.6 | 0.6 | 0.5 | 0.6 | 0.5 | 0.6 |
+0.51 | 0.6 | 0.5 | 0.6 | 0.6 | 0.5 | 0.5 | 0.5 | 0.5 |
+0.45 | 0.5 | 0.4 | 0.5 | 0.54 | 0.4 | 0.5 | 0.4 | 0.4 |
-0.50 | -0.5 | -0.5 | -0.5 | -0.5 | -0.5 | -0.5 | -0.5 | -0.5 |
-0.51 | -0.5 | -0.6 | -0.6 | -0.6 | -0.5 | -0.5 | -0.5 | -0.5 |
decimalモジュールで精度と丸め方式を指定する
精度と丸め方式をいろいろ試す場合は、次のようにquantize
メソッドを使うと便利です。
In [1]: from decimal import * In [2]: Decimal('0.51').quantize(Decimal('0.0'), rounding=ROUND_UP) Out[2]: Decimal('0.6') In [3]: Decimal('0.51').quantize(Decimal('0.0'), rounding=ROUND_05UP) Out[3]: Decimal('0.6') In [4]: Decimal('0.61').quantize(Decimal('0.0'), rounding=ROUND_UP) Out[4]: Decimal('0.7') In [5]: Decimal('0.61').quantize(Decimal('0.0'), rounding=ROUND_05UP) Out[5]: Decimal('0.6')
おわりに
複数の経理システムをまたいたサービスを作る場合は、丸め処理の違いが思わない形で表面化します。 誤差の原因を追求し、テストケースやFAQに落とし込み、平穏な月初を迎えられるようにしましょう。